/**
 * 
 */
package it.unical.canonaco.rende.igpe.game.cbv.logiclayer.engine.actions;

import java.util.Iterator;
import java.util.LinkedList;

import it.unical.canonaco.rende.igpe.game.cbv.datastructure.TreeNode;
import it.unical.canonaco.rende.igpe.game.cbv.logiclayer.engine.GUIDStorage;
import it.unical.canonaco.rende.igpe.game.cbv.logiclayer.engine.component.pawn.Character;
import it.unical.canonaco.rende.igpe.game.cbv.logiclayer.engine.component.pawn.stats.Stats;
import it.unical.canonaco.rende.igpe.game.cbv.logiclayer.engine.component.player.SimplePlayer;
import it.unical.canonaco.rende.igpe.game.cbv.logiclayer.engine.component.world.Chessboard;
import it.unical.canonaco.rende.igpe.game.cbv.logiclayer.engine.events.EventManager;
import it.unical.canonaco.rende.igpe.game.cbv.logiclayer.engine.events.FinalMovementEventInformation;
import it.unical.canonaco.rende.igpe.game.cbv.logiclayer.engine.events.InvalidPointSettingEvent;
import it.unical.canonaco.rende.igpe.game.cbv.logiclayer.engine.events.MovementEvent;
import it.unical.canonaco.rende.igpe.game.cbv.logiclayer.engine.events.SelectedCharacterEvent;

/**
 * @author Massimo Canonaco
 */
public class MovementAction extends AbstractCharacterAction {

	/**It's the path for reaching the destination point. */
	private LinkedList<ChessboardPointCoordinate> movementRoute;
	
	/**It's the starting point of the movement action. It's used for calculating the distance from the start to the end. */
	private ChessboardPointCoordinate steadyStarting;
	
	public final static float GUID = GUIDStorage.MOVEMENT_ACTION;
	
	/** */
	public MovementAction(SimplePlayer performer,Chessboard arena) {
		super(performer,arena);
		movementRoute = new LinkedList<>();
		steadyStarting = new ChessboardPointCoordinate();
		end = true;
	}
	
	/* (non-Javadoc)
	 * @see it.unical.canonaco.rende.igpe.game.cbv.logiclayer.engine.actions.Action#getActionIdentifier()
	 */
	@Override
	public float getActionIdentifier() {
		return GUID;
	}

	/* (non-Javadoc)
	 * @see it.unical.canonaco.rende.igpe.game.cbv.logiclayer.engine.actions.Action#reset()
	 */
	@Override
	public void reset() {
		
		if(!movementRoute.isEmpty())
		{
			movementRoute.clear();
		}
		
		end = true;
		character = null;
		steadyStarting.reset();
		targetPoint.reset();
	}
	
	/**Checks if there's at least one route from the <i>from point</i> to the <i>to point</i>. 
	 * If it exists, the class attribute <i>movementRoute</i> will contain all the points of that route.
	 * The starting point is the initial character position.
	 * @param to 		is the destination point
	 * @return True - the route does not exists. <br/>
	 * False - the route  exists. */
	private boolean findRoute(ChessboardPointCoordinate to)
	{
//		System.out.println("!!!!!!!!!!!!!!!"+character.getRow()+" "+character.getColumn()+ " a " + to.row+" "+to.column+"!!!!!!!!!!!!!!!");
		// All characters must move into a circle area, it depends on the character's movement range.
		// Every change of position reduces the movement range in the actual player turn.
		final float radius = character.getStats().getCharacteristics().getMovementRange(); 
		
		// fRow is the from row (starting row) and fColumn is the from column (starting column). This starting point is the actual character position.
		final int fRow = character.getRow(), fColumn = character.getColumn();
		
		// For calculating route is used a support boolean matrix. Its dimension will be the exact movement that the character could do into his movement range.
		final int left = fColumn - radius > 0 ? Math.round(fColumn - radius) : 0,
				right = fColumn + radius < park.getSize() ? Math.round(fColumn + radius) : park.getSize() -1,
				top = fRow - radius > 0 ? Math.round(fRow - radius) : 0,
				bottom = fRow + radius < park.getSize() ? Math.round(fRow + radius) : park.getSize() -1,
				sRow = fRow -  top, sColumn = fColumn - left;
				
		// Using the previous variables we can set right dimension for the support matrix, +2 means the actual cell on which the character is located+borders.
		// True value means we don't use the cell, so it's unvisited; false otherwise and there's no reason for using it again trying to calculate the route.
		boolean[][] unvisitedField = new boolean[right-left+2][bottom-top+2]; 
		
//		boolean[][] unvisitedField = new boolean[park.getSize()][park.getSize()];
		// Set all the cells to true
		for(int row = 0; row < unvisitedField.length; row++)
		{
			for(int column = 0; column < unvisitedField[0].length; column++)
			{
				unvisitedField[row][column] = true;
			}
		}
		
		// For reaching the destination it's used a tree, every node will have closest cells on which the character could go on.
		TreeNode<ChessboardPointCoordinate> actual = new TreeNode<>();
		
		// Set the root to the actual position of the Character.
		actual.setValue(new ChessboardPointCoordinate(fRow, fColumn));
		
		// Make it visited.
		unvisitedField[sRow][sColumn] = false;
		
		// True means we didn't find the route, false otherwise.
		boolean distant = true;

		do
		{
			
//			for (ChessboardPointCoordinate bs : movementRoute) 
//			{
//				System.out.println("percorso attuale: " +bs.row+" "+bs.column);
//			}
			
			// If the actual node is the destination point. We find it, so it's not 'distant' anymore.
			if(actual.getValue().equals(to))
			{
				// Set distant to false.
				distant = false;
				
				// Add the target point to the route list.
				movementRoute.addLast(to);
				
//				System.out.println("trovato il punto arrivo"+to.row+" "+to.column);
			}
			// We are still looking for the destination point, so the actual node (point) is not equals to the destination.
			else
			{
				// If from this point we can go on other close points.
				if(newExploringPoints(actual,unvisitedField,top,left))

				{
					// Add this point to the route list.
					movementRoute.addLast(actual.getValue());
					
//					System.out.println(" .o meglio "+actual.getValue().row+" "+actual.getValue().column);
					// Set actual to the first close points, his first child.
					actual = actual.getFirstChild();
//					System.out.println("sono a "+actual.getValue().row+" "+actual.getValue().column);
//					System.out.println("padre: "+actual.getParent().getValue().row+" "+actual.getParent().getValue().column);
				}
				// If from this point we can't go on other close points (they could be out of reach or we met yet).
				else
				{
					if(actual.hasChildren())
					{
						actual = actual.getFirstChild();
					}
					else if(!actual.hasChildren()){
					// Take the node's parent.
					TreeNode<ChessboardPointCoordinate> father = actual.getParent();
					
					// If actual node is not the root node (starting point), his parent is not null
					if(father != null) 
					{
						// Remove the actual node from the father's children list
						TreeNode<ChessboardPointCoordinate> rimosso = father.removeFirstChild();
						
//						System.out.println("Figlioletto levato : "+rimosso.getValue().row+" "+rimosso.getValue().column);
//						System.out.println("Padre : "+father.getValue().row+" "+father.getValue().column);
						// If father has children, try to find the path using the first child of the children list.
						if(father.hasChildren())
						{
							// Make actual node to the brother of the old (removed) node, so his brother is the first child now.
							actual = father.getFirstChild();
						}
						// Father node does not have any children for trying to find the route, so it's not useful for the 'route list'
						else
						{
//							System.out.println("father : "+father.getValue().row+" "+father.getValue().column);
							// Remove the father node (point) from the route list.
							movementRoute.removeLast();
//							ChessboardPointCoordinate last = movementRoute.removeLast();
							
//							System.out.println("levo last (uguale father): "+last.row+" "+last.column);
							// Make actual node to the father of the 'father'.
							actual = father.getParent();
							while(actual != null && !actual.hasChildren())
							{
								movementRoute.removeLast();
								actual = father.getParent();
							}
						}
					}
					}
					// Actual node is the root and it does not have any children 
					// because they were all popped out for not being useful to reach the destination or just because it did not have any near useful cells.
					// If it had some children, it was been add to the route list so in this case route is still inside it and must be removed.
					// If the starting point doesn't have children since the start, it won't be inserted in the route list, so it is empty yet.
					else if(!movementRoute.isEmpty())
					{
						// The source point (root node) does not have any children for trying to reach the destination so remove it from route list.
						// It doesn't exist any path for the target point, so make route list empty.
						movementRoute.removeLast();
					}
				}
			}
		}while(!movementRoute.isEmpty() && distant); // if we used all the possible cells or we have just found the route for the destination point.
		
//		System.out.println("e......e");
//		for (ChessboardPointCoordinate bs : movementRoute) {
//			System.out.println("..." +bs.row+" "+bs.column);
//		}
		return distant;
	}
	
	/**Checks if there's near cells which are unvisited.
	 * @param actual		is the center cell. Visited cells are calculated from this one.
	 * @param unvisited 	is the matrix which contains the visited/unvisited cells.
	 * @param sRow			is row value used for porting from real cell to unvisited matrix.
	 * @param sCol 			is column value used for porting from real cell to unvisited matrix.
	 * @return True exists at least one unvisited cell; false otherwise. */
	private boolean newExploringPoints(TreeNode<ChessboardPointCoordinate> actual, boolean[][] unvisited, int sRow,int sCol) 
	{
		// This value will be returned at end of function.
		boolean newChild = false;
		
		// Get and set row and column values for porting.
		int aRow = actual.getValue().row, aCol = actual.getValue().column,
			vRow = aRow-sRow, vCol = aCol - sCol, tvRow = 0, tvCol= 0,taRow = 0, taCol = 0;
		
		// Checking closest cells 
		for(int rowIndex = -1; rowIndex < 2; rowIndex++)
		{
			for(int columnIndex = -1; columnIndex < 2; columnIndex++)
			{
				// Row for the unvisited matrix
				tvRow = vRow+rowIndex; 
				
				// Column for the unvisited matrix
				tvCol = vCol+columnIndex;
				
				// Row for the board (arena/map)
				taRow = aRow+rowIndex;
				
				// Column for the board (arena/map)
				taCol = aCol+columnIndex;
				
//				System.out.println("...CONTROLLO la cella "+taRow+" "+taCol);
//				if(tvRow >= 0 && tvRow < unvisited.length && tvCol >= 0 && tvCol < unvisited[0].length )
//				{
//					System.out.println(". unv "+unvisited[tvRow][tvCol]);
//				}
//				else
//				{
//					System.out.println(". non possibile in TV");
//				}
//				System.out.println(". in range "+character.isInMovementRange(taRow, taCol));
//				System.out.println(". walkable "+park.isWalkable(taRow, taCol));
//				System.out.println(". free "+(this.getCharacter(taRow, taCol) == null));
				// If our cell (vRow+rowIndex, vCol+columnIndex) is a valid cell for unvisited matrix 
				// and it's not visited yet and it's in character's range and it's walkable and there aren't other characters
				if(tvRow >= 0 && tvRow < unvisited.length && tvCol >= 0 && tvCol < unvisited[0].length 
//				if(taRow >= 0 && taRow < unvisited.length && taCol >= 0 && taCol < unvisited[0].length 
					&& unvisited[tvRow][tvCol]
//					&& unvisited[taRow][taCol]
					&& character.isInMovementRange(taRow, taCol)
					&& park.isWalkable(taRow, taCol) 
					&& this.getCharacter(taRow, taCol) == null)
				{
					// Create a new point for the value of the actual node's child.
					ChessboardPointCoordinate coordinate = new ChessboardPointCoordinate();
					
					// Set row value.
					coordinate.row = taRow;
					
					// Set column value.
					coordinate.column = taCol;
					
					// Create a node point for the tree.
					TreeNode<ChessboardPointCoordinate> child = new TreeNode<>();
					
					// Set node's value.
					child.setValue(coordinate);
					
					// Add new child node to his parent, the actual node.
					actual.addChild(child);
					
					// Mark this point as visited.
					unvisited[tvRow][tvCol] = false;
					
					// Signal a new child has just been found.
					newChild = true;
					
				}
			}
		}
		
		return newChild;
	}

	/** */
	synchronized private void setting(LinkedList<GenericActionInfo> infos)
	{
//		System.out.println("setting");
		Iterator<GenericActionInfo> iterator = infos.iterator();
		GenericActionInfo genericEvent = null;
		boolean cicle = true;
		
		// Look through info list.
		while(iterator.hasNext() && cicle)
		{
			// Get the next information.
			genericEvent = iterator.next();
			
			// If it's for coordinate.
			if(genericEvent.getGUID() == ActionCoordinateInfo.GUID)
			{
				// Remove from the list.
				iterator.remove();
				
				// If character is not set.
				if(character == null)
				{
					

					// Set as starting point and get the character.
					ChessboardPointCoordinate temp = ((ActionCoordinateInfo)genericEvent).getPoint();
//					System.out.println("source "+temp.row+" "+temp.column);
					steadyStarting.row = temp.row;
					steadyStarting.column = temp.column;
					
					character = this.getCharacter(steadyStarting.row, steadyStarting.column);
					// If there're no characters on that point, or he's not owned by the performer player.
					if(character == null || character.getOwner() != performer)
					{
						System.out.println("invalid source "+(character == null)+" "+(character.getOwner() != performer));
						System.out.println(character.getOwner().getNickname()+" "+performer.getNickname());
						// Set the character to null.
						character = null;
						
						// Signal an invalid source is passed.
						EventManager.getInstance().triggerEvent(new InvalidPointSettingEvent(true, steadyStarting.row, steadyStarting.column));
						
						// Reset the initial point.
						steadyStarting.reset();
//						System.out.println("errore ");
						
						// Stop setting.
						cicle = false;
					}
					else
					{
//						System.out.println("raggio in movment "+character.getStats().getCharacteristics().getMovementRange());
						EventManager.getInstance().triggerEvent(new SelectedCharacterEvent(character, steadyStarting.row, steadyStarting.column, character.getStats().getCharacteristics().getMovementRange()));
					}
				}
				// Else if we set the source, set the target.
				else //if(targetPoint.row != character.getRow() || targetPoint.column != character.getColumn())
				{
					// Get and set the target point.
					ChessboardPointCoordinate temp = ((ActionCoordinateInfo)genericEvent).getPoint();
					targetPoint.row = temp.row;
					targetPoint.column = temp.column;

					Character characterOnTarget = this.getCharacter(targetPoint.row, targetPoint.column);
					System.out.println("target "+temp.row+" "+temp.column);
					// If there's a character on the target point and it's owned by performer this is another source coordinate.
					if(characterOnTarget != null && characterOnTarget.getOwner() == performer)
					{
						// Reset the starting point.
						steadyStarting.row = targetPoint.row;
						steadyStarting.column = targetPoint.column;
						
						// Reset the target point.
						targetPoint.reset();
						
						// Set character to the new one.
						character = characterOnTarget;
//						System.out.println("raggio in movment "+character.getStats().getCharacteristics().getMovementRange());
						EventManager.getInstance().triggerEvent(new SelectedCharacterEvent(character, steadyStarting.row, steadyStarting.column, character.getStats().getCharacteristics().getMovementRange()));
//						System.out.println("cambio soggetto");
//						if(character.getStats().getCharacteristics() != null)
//							System.out.println("ha le stats");
					}
					// Else if destination isn't walkable or it is not in range or it's already occupied or there's no path for reaching that point.
					else if(!park.isWalkable(targetPoint.row, targetPoint.column) 
							|| !character.isInMovementRange(targetPoint.row, targetPoint.column)
							|| characterOnTarget != null
							|| this.findRoute(targetPoint))
					{			
						// Throw invalid destination event
						EventManager.getInstance().triggerEvent(new InvalidPointSettingEvent(false, targetPoint.row, targetPoint.column));

						// Reset the target point.
						targetPoint.reset(); 
						
						// Stop setting.
						cicle = false;
						
						System.out.println("targetpoint non valido");
					}
				}
				// Release the information.
				genericEvent.setBusy(false);
			}
		}
		
		// If we met an "error".
		if(!cicle)
		{
			//TODO: release all information in this error case.
			// Clear the info list.
			infos.clear();
		}
	}
	
	/** */
	@Override
	synchronized public boolean execute(LinkedList<GenericActionInfo> infos) {
		
//		System.out.println("exectue "+infos.size());
		// When movement action is started end is set to false. This could also mean if end is true we don't set all the values
		// so execution of the action is not already started. If it's not started we have to set his values.
		if(end)
		{
			// Set the variables.
			setting(infos);
			
			// If target is not set we can stop the execution of the action and return false, for waiting the ActionCoord which'll set the target.
			if(character == null || targetPoint == null)
			{
//				System.out.println("non eseguita");
				return false;
			}
		}
		
		// Actions for moving the character for reaching destination
		if(!movementRoute.isEmpty())
		{
			// Character must be moved into the destination point, so this action is not finished.
			end = false;
			
			// Into movementRoute are contained all coordinates for reaching the meta point (included the source point and the meta one). 
			// So the first element into this list is the actual position of the Character and the successive element is the next point where to go.
			// Get the actual position of the character.
			ChessboardPointCoordinate point = movementRoute.pop();
			
			// If out character isn't on the last point, there's at least another point.
			// So he must be moved to the next step.
			if(!movementRoute.isEmpty())
			{
				// Copy the point values, for the movement event.
				ChessboardPointCoordinate previous = new ChessboardPointCoordinate(point.row, point.column);
				
				// Drop the character from the chessboard (arena).
				park.dropPawn(point.row, point.column);

				// Get the next point where to place him.
				point = movementRoute.getFirst();

				// Change position and update the chessboard (arena).
				character.setPosition(point.row, point.column);

				// Update (add) the character to the chessboard (arena).
				park.addPawn(point.row, point.column, character);
				
				// Throw the "movement event"
				EventManager.getInstance().triggerEvent(new MovementEvent(character, point.row, point.column, previous.row, previous.column));
			}
			// Else character has just reached the destination point, so the action is ended.
			// Finally we can shrink his movement range.
			// P.S. Range is reduced here because that's not matter how many steps character does for reaching the final point,
			// but important thing vector length between the starting point and the end point.
			else
			{
				// Make end true.
				end = true;
				
				// Calculate the between the starting point and the destination point.
				final float dX = steadyStarting.row-character.getRow(), 
							dY = steadyStarting.column - character.getColumn(),
							travelledDistance = (float) Math.sqrt((dX*dX)+(dY*dY));
				
				// Update the travel distance variable of the character.
				character.setTraveled(character.getTraveled()+travelledDistance);
				
				// Get the actual statics of the character.
				Stats stats = character.getStats().getCharacteristics();
				
				// Update the character's movement range with the shrank range: actual range - distance.
				stats.setMovementRange(stats.getMovementRange()-travelledDistance);
				
				EventManager.getInstance().triggerEvent(new FinalMovementEventInformation(steadyStarting.row, steadyStarting.column, character.getRow(), character.getColumn()));
			}
			return true;
		}
		return false;
	}

	@Override
	public String getName() 
	{
		return "MOVEMENT";
	}
}
